// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jing

#pragma once

#include <string>

#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"
#include "coin2/exchange/base/feed_util/wallet.h"

namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using namespace std::literals;

class UpbitFeedUpdateParser {
 public:
  static void ParseBook(BookBuilder* book_builder_, const rapidjson::Document& book_json) {
    auto ts = stoull(book_json["timestamp"].GetString()) * 1'000'000LL;
    auto tx = book_builder_->NewTransaction(ts);
    if (book_json["stream_type"].GetString() == "SNAPSHOT"sv) {
      tx.Clear();
    }

    for (const auto& unit : book_json["orderbook_units"].GetArray()) {
      if (unit.HasMember("ask_price") && unit.HasMember("ask_size")) {
        // Upbit allow big ask price, for instance
        // {"ask_price":8.9E10,"bid_price":4315000.0,"ask_size":0.005,"bid_size":0.04328774}
        // We must filter out those big price cases because in fastfeed price
        // will be multiplied by 1e9 and thus may overflow.
        double price = stod(unit["ask_price"].GetString());
        if (price > 9223372036) {
          continue;
        }
        tx.UpdateAsk(price, stod(unit["ask_size"].GetString()));
      }
      if (unit.HasMember("bid_price") && unit.HasMember("bid_size")) {
        tx.UpdateBid(
            stod(unit["bid_price"].GetString()),
            stod(unit["bid_size"].GetString()));
      }
    }
  }

  static void ParseTrade(const rapidjson::Value& trade_json, Trade* trade) {
    trade->price = stod(trade_json["trade_price"].GetString());
    trade->fill_qty = stod(trade_json["trade_volume"].GetString());
    trade->side = ParseTradeSide(trade_json["ask_bid"].GetString());
    trade->timestamp = stoull(trade_json["trade_timestamp"].GetString()) * 1'000'000LL;
  }

  static coin2::exchange::feed::TradeSide ParseTradeSide(const std::string& side) {
    if (side == "BID") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "ASK") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[Upbit] unknown side: " + side);
    }
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(std::string_view channel) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"minutes/1?", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"minutes/3?", fastfeed::proto::KLINE_INTERVAL_3MIN},
      {"minutes/5?", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"minutes/10?", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"minutes/15?", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"minutes/30?", fastfeed::proto::KLINE_INTERVAL_30MIN},
      {"minutes/60?", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"minutes/240?", fastfeed::proto::KLINE_INTERVAL_4HOUR},
      {"days?", fastfeed::proto::KLINE_INTERVAL_1DAY},
      {"weeks?", fastfeed::proto::KLINE_INTERVAL_1WEEK},
      {"months?", fastfeed::proto::KLINE_INTERVAL_1MON},
    };
    auto iter = interval_map.find(std::string(channel));
    if (iter == interval_map.end()) {
      NOTREACHED() << "Not support interval " << channel;
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }

  static bool ParseKline(
      const rapidjson::Value& kline_json,
      Klines* klines,
      int64_t timestamp,
      fastfeed::proto::KlineInterval interval) {
    int64_t open_timestamp = TimestampFromIso8601(kline_json["candle_date_time_utc"].GetString());
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    kline->open_timestamp = open_timestamp;
    kline->open = stod(kline_json["opening_price"].GetString());
    kline->high = stod(kline_json["high_price"].GetString());
    kline->low = stod(kline_json["low_price"].GetString());
    kline->close = stod(kline_json["trade_price"].GetString());
    kline->volume = stod(kline_json["candle_acc_trade_volume"].GetString());
    kline->turnover = kline->volume * stod(kline_json["candle_acc_trade_price"].GetString());
    kline->timestamp = timestamp;
    kline->interval = interval;
    return freezed;
  }

  static void ParseWallet(const rapidjson::Value& transaction, Wallet* wallet) {
    const char* wallet_state = "null";
    if (transaction["wallet_state"].IsString()) {
      wallet_state = transaction["wallet_state"].GetString();
    }
    if (strcmp(wallet_state, "working") == 0) {
      wallet->can_deposit = true;
      wallet->can_withdraw = true;
    } else if (strcmp(wallet_state, "withdraw_only") == 0) {
      wallet->can_deposit = false;
      wallet->can_withdraw = true;
    } else if (strcmp(wallet_state, "deposit_only") == 0) {
      wallet->can_deposit = true;
      wallet->can_withdraw = false;
    } else {
      wallet->can_deposit = false;
      wallet->can_withdraw = false;
    }
    const char* block_state = "null";
    if (transaction["block_state"].IsString()) {
      block_state = transaction["block_state"].GetString();
    }
    if (strcmp(block_state, "normal") == 0) {
      wallet->blockchain_delay = false;
    } else {
      wallet->blockchain_delay = true;
    }
  }
};
}  // namespace impl

using impl::UpbitFeedUpdateParser;
